Here are some Julia usages to create calculus objects.
The Julia packages loaded below are all loaded when the CalculusWithJulia package is loaded.
A Julia package is loaded with the using command:
using LinearAlgebra
The LinearAlgebra package comes with a Julia installation. Other packages can be added. Something like:
using Pkg Pkg.add("SomePackageName")
These notes have an accompanying package, CalculusWithJulia, that when installed, as above, also installs most of the necessary packages to perform the examples.
Packages need only be installed once, but they must be loaded into each session for which they will be used.
using CalculusWithJulia
Packages can also be loaded through import PackageName. Importing does not add the exported objects of a function into the namespace, so is used when there are possible name collisions.
Objects in Julia are "typed." Common numeric types are Float64, Int64 for floating point numbers and integers. Less used here are types like Rational{Int64}, specifying rational numbers with a numerator and denominator as Int64; or Complex{Float64}, specifying a comlex number with floating point components. Julia also has BigFloat and BigInt for arbitrary precision types. Typically, operations use "promotion" to ensure the combination of types is appropriate. Other useful types are Function, an abstract type describing functions; Bool for true and false values; and Sym for symbolic values (through SymPy).
For the most part the type will not be so important, but it is useful to know that for some function calls the type of the argument will decide what method ultimately gets called. (This allows symbolic types to interact with Julia functions in an idiomatic manner.)
Functions can be defined four ways:
one statement functions follow traditional mathematics notation:
f(x) = exp(x) * 2x
f (generic function with 1 method)
multi-statement functions are defined with the function keyword. The end statement ends the definition. The last evaluated command is returned. There is no need for explicit return statement, though it can be useful for control flow.
function g(x) a = sin(x)^2 a + a^2 + a^3 end
g (generic function with 1 method)
Anonymous functions are useful for example, as arguments to other functions or as return values.
using ForwardDiff # add-on package to find numeric derivatives using automatic differentiation D(f::Function) = x -> ForwardDiff.derivative(f, x)
D (generic function with 1 method)
The above turns a function that finds the derivative of f at a point and creates a function, D, returning a derivative function.
Anonymous function may also be created using the function keyword.
For mathematical functions $f: R^n \rightarrow R^m$ when $n$ or $m$ is bigger than 1 we have:
\[ n =1 \]
by $m > 1$ use a "vector" for the return value
r(t) = [sin(t), cos(t), t]
r (generic function with 1 method)
\[ n > 1 \]
by $m=1$ use multiple arguments
f(x,y,z) = x*y + y*z + z*x f(v) = f(v...)
f (generic function with 2 methods)
Some functions need to pass in a container of values, for this the last definition is useful to expand the values. Splatting takes a container and treats the values like individual arguments.
Alternatively, indexing can be used directly, as in:
f(x) = x[1]*x[2] + x[2]*x[3] + x[3]*x[1]
f (generic function with 2 methods)
For vector fields ($n,m > 1$) a combination is used:
F(x,y,z) = [-y, x, z] F(v) = F(v...)
F (generic function with 2 methods)
Functions are called using parentheses to group the arguments.
f(t) = sin(t)*sqrt(t) sin(1), sqrt(1), f(1)
(0.8414709848078965, 1.0, 0.8414709848078965)
Julia can have many methods for a single generic function. (E.g., it can have many different implementations of addiion when the + sign is encountered.) The type of an argument and the number of arguments are used for dispatch.
Here the number of arguments is used:
Area(w, h) = w * h # area of rectangle Area(w) = Area(w, w) # area of square using area of rectangle defintion
Area (generic function with 2 methods)
Calling Area(5) will call Area(5,5) which will return 5*5.
Similarly, the definition for a vector field:
F(x,y,z) = [-y, x, z] F(v) = F(v...)
F (generic function with 2 methods)
takes advantage of multiple dispatch to allow either a vector argument or individual arguments.
Type parameters can be used to restrict the type of arguments that are permitted. The D(f::Function) definition illustrates how the D function is restricted to Function objects.
Optional arguments may be specified with keywords, when the function is defined to use them. Keywords are separated from positional arguments using a semicolon, ;:
circle(x; r=1) = sqrt(r^2 - x^2) circle(0.5), circle(0.5, r=10)
(0.8660254037844386, 9.987492177719089)
The add-on SymPy package allows for symbolic expressions to be used. Symbolic values are defined with @vars, as below.
using SymPy @vars x y z # no comma x^2 + y^3 + z
Assumptions on the variables can be useful, particularly with simplification, as in
@vars x y z real=true
(x, y, z)
Symbolic expressions flow through Julia functions symbolically
sin(x)^2 + cos(x)^2
Numbers are symbolic once SymPy interacts with them:
x - x + 1 # 1 is now symbolic
The number PI is a symbolic pi. a
sin(PI), sin(pi)
(0, 1.2246467991473532e-16)
Use Sym to create symbolic numbers, N to find a Julia number from a symbolic number:
1 / Sym(2)
N(PI)
π = 3.1415926535897...
Many Julia functions will work with symbolic objects through multiple dispatch (e.g., sin, cos, ...). Sympy functions that are not in Julia can be accessed through the sympy object using dot-call notation:
sympy.harmonic(10)
We use a few different containers:
Tuples. These are objects grouped together using parentheses. They need not be of the same type
x1 = (1, "two", 3.0)
(1, "two", 3.0)
Tuples are useful for programming. For example, they are uesd to return multiple values from a function.
Vectors. These are objects of the same type (typically) grouped together using square brackets, values separated by commas:
x2 = [1, 2, 3.0] # 3.0 makes theses all floating point
3-element Array{Float64,1}:
1.0
2.0
3.0
Matrices. Like vectors, these have the same type, only they are 2-dimensional. Use spaces to separate values along a row; semicolons to separate rows:
x3 = [1 2 3; 4 5 6; 7 8 9]
3×3 Array{Int64,2}:
1 2 3
4 5 6
7 8 9
Row vectors. A vector is 1 dimensional, though it may be identified as a column of two dimensional matrix. A row vector is a two-dimensional matrix with a single row:
x4 = [1 2 3.0]
1×3 Array{Float64,2}:
1.0 2.0 3.0
These have indexing using square brackets:
x1[1], x2[2], x3[3]
(1, 2.0, 7)
Matrices are usually indexed by row and column:
x3[1,2] # row one column two
2
For vectors and matrices – but not tuples – indexing can be used to change a value in the container:
x2[1], x3[1,1] = 2, 2
(2, 2)
Vectors and matrices are arrays. Arrays have mathematical operations, such as addition and subtraction, defined for them. Tuples do not.
Destructuring is another way to get at the entries:
a,b,c = x2
3-element Array{Float64,1}:
2.0
2.0
3.0
An arithmetic progression, $a, a+h, a+2h, ..., b$ can be produced efficiently using the range operator:
5:10:55 # an object that describes 5, 15, 25, 35, 45, 55
5:10:55
If h=1 it can be omitted:
1:10 # an object that describes 1,2,3,4,5,6,7,8,9,10
1:10
The range function can efficiently describe $n$ evenly spaced points between a and b:
range(0, pi, length=5) # range(a, stop=b, length=n) for version 1.0
0.0:0.7853981633974483:3.141592653589793
This is useful for creating regularly spaced values needed for certain plots.
The for keyword is useful for iteration, Here is a traditional for loop, as i loops over each entry of the vector [1,2,3]:
for i in [1,2,3] print(i) end
123
List comprehensions are similar, but are useful as they perform the iteration and collect the values:
[i^2 for i in [1,2,3]]
3-element Array{Int64,1}:
1
4
9
Comprehesions can also be used to make matrices
[1/(i+j) for i in 1:3, j in 1:4]
3×4 Array{Float64,2}:
0.5 0.333333 0.25 0.2
0.333333 0.25 0.2 0.166667
0.25 0.2 0.166667 0.142857
Comprehensions apply an expression to each entry in a container through iteration. Applying a function to each entry of a container can be facilitated by:
Broadcasting. Using . before an operation instructs Julia to match up sizes (possibly extending to do so) and then apply the operation element by element:
xs = [1,2,3] sin.(xs) # sin(1), sin(2), sin(3) bases = [5,5,10] log.(bases, xs) # log(5, 1), log(5,2), log(10, 3)
3-element Array{Float64,1}:
0.0
0.43067655807339306
0.47712125471966244
Row and column vectors can fill in:
ys = [4 5] # a row vector f(x,y) = (x,y) f.(xs, ys) # broadcasting a column and row vector makes a matrix, then applies f.
3×2 Array{Tuple{Int64,Int64},2}:
(1, 4) (1, 5)
(2, 4) (2, 5)
(3, 4) (3, 5)
The map function applies a function to each element:
map(sin, [1,2,3])
3-element Array{Float64,1}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
The following commands use the Plots package. The Plots package expects a choice of backend. We will use both plotly and gr (and occasionally pyplot()).
using Plots plotly() # select plotly. Use `gr()` for GR
Plots.PlotlyBackend()
Plotting a univariate function $f:R \rightarrow R$
using plot(f, a, b)
plot(sin, 0, 2pi)
Or
f(x) = exp(-x/2pi)*sin(x) plot(f, 0, 2pi)
Or with an anonymous function
plot(x -> sin(x) + sin(2x), 0, 2pi)
note("""The time to first plot can be lengthy!. This can be removed by creating a custom `Julia` image, but that is no introductory level stuff. As well, standalone plotting packages offer quicker first plots, but the simplicity of `Plots` is preferred. Subsequent plots are not so time consuming, as the initial time is spent compiling functions so their re-use is speedy. """)
The time to first plot can be lengthy!. This can be removed by creating a custom Julia image, but that is no introductory level stuff. As well, standalone plotting packages offer quicker first plots, but the simplicity of Plots is preferred. Subsequent plots are not so time consuming, as the initial time is spent compiling functions so their re-use is speedy.
Arguments of interest include
| Attribute | Value |
|---|---|
legend | A boolean, specify false to inhibit drawing a legend |
aspect_ratio | Use :equal to have x and y axis have same scale |
linewidth | Ingters greater than 1 will thicken lines drawn |
color | A color may be specified by a symbol (leading :). |
E.g., :black, :red, :blue |
using plot(xs, ys)
The lower level interface to plot involves creating x and y values to plot:
xs = range(0, 2pi, length=100) ys = sin.(xs) plot(xs, ys, color=:red)
plotting a symbolic expression
A symbolic expression of single variable can be plotted as a function is:
@vars x plot(exp(-x/2pi)*sin(x), 0, 2pi)
Multiple functions
The ! Julia convention to modify an object is used by the plot command, so plot! will add to the existing plot:
plot(sin, 0, 2pi, color=:red) plot!(cos, 0, 2pi, color=:blue) plot!(zero, color=:green) # no a, b then inherited from graph.
The zero function is just 0 (useful when the type of a number is important).
Plotting a parameterized (space) curve function $f:R \rightarrow R^n$, $n = 2$ or $3$
Using plot(xs, ys)
Let $f(t) = e^{t/2\pi} \langle \cos(t), \sin(t)\rangle$ be a parameterized function. Then the $t$ values can be generated as follows:
ts = range(0, 2pi, length = 100) xs = [exp(t/2pi) * cos(t) for t in ts] ys = [exp(t/2pi) * sin(t) for t in ts] plot(xs, ys)
using plot(f1, f2, a, b). If the two functions describing the components are available, then
f1(t) = exp(t/2pi) * cos(t) f2(t) = exp(t/2pi) * sin(t) plot(f1, f2, 0, 2pi)
Using plot_parametric_curve. If the curve is described as a function of t with a vector output, then the CalculusWithJulia package provides plot_parametric_curve to produce a plot:
r(t) = exp(t/2pi) * [cos(t), sin(t)] plot_parametric_curve(r, 0, 2pi)
The low-level approach doesn't quite work as easily as desired:
ts = range(0, 2pi, length = 4) vs = r.(ts)
4-element Array{Array{Float64,1},1}:
[1.0, 0.0]
[-0.697806, 1.20864]
[-0.973867, -1.68679]
[2.71828, -6.65787e-16]
As seen, the values are a vector of vectors. To plot a reshaping needs to be done:
ts = range(0, 2pi, length = 100) vs = r.(ts) xs = [vs[i][1] for i in eachindex(vs)] ys = [vs[i][2] for i in eachindex(vs)] plot(xs, ys)
This approach is faciliated by the unzip function in CalculusWithJulia:
plot(unzip(vs)...)
Plotting an arrow
An arrow in 2D can be plotted with the quiver command. We show the arrow(p, v) (or arrow!(p,v) function) from the CalculusWithJulia package, which has an easier syntax:
gr() plot_parametric_curve(r, 0, 2pi) t0 = pi/8 arrow!(r(t0), r'(t0))
The GR package makes nicer arrows that Plotly.
Plotting a scalar function $f:R^2 \rightarrow R$
The surface and contour functions are available to visualize a scalar function of $2$ variables:
A surface plot
(The plotly backend allows for rotation by the mouse.)
plotly() f(x, y) = 2 - x^2 + y^2 xs = ys = range(-2,2, length=25) surface(xs, ys, f)
The function generates the $z$ values, this can be done by the user and then passed to the surface(xs, ys, zs) format:
surface(xs, ys, f.(xs', ys))
A contour plot
The contour function is like the surface function.
contour(xs, ys, f)
contour(xs, ys, f.(xs', ys))
An implicit equation. The constraint $f(x,y)=c$ generates an implicit equation. While contour can be used for this plot by adjusting the requested contours, the ImplicitEquations package can as well, and perhaps is easier. This package is loaded with CalculusWithJulia; loading it by itself will lead to naming conflicts with SymPy, so best not to do so. ImplicitEquations plots predicates formed by Eq, Le, Lt, Ge, and Gt (or some unicode counterparts). For example to plot when $f(x,y) = \sin(xy) - \cos(xy) \leq 0$ we have:
f(x,y) = sin(x*y) - cos(x*y) plot(Le(f, 0))